package org.ii.dataprovider.dapper;

import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import org.ii.dataprovider.dapper.Exceptions.DappExecutionErrorException;
import org.ii.dataprovider.dapper.Exceptions.DappNotFoundException;
import org.ii.dataprovider.dapper.Exceptions.InvalidDappOutputException;
import org.ii.dataprovider.dapper.Exceptions.InvalidVariableArgumentsException;
import org.ii.dataprovider.dapper.Exceptions.NetworkErrorException;


/**
 * A Java SDK for Dapper.
 * This class lets you access Dapp output from Java.
 * Please see examples/RunDapps.java for more information.
 * 
 * @version 0.3
 * 
 * @author Yoav/Joey Freund and Jon Aizen
 */
public class Dapp {
	
	
	
	//-------------------------------------------------------------------------
	
	// The host of the Dapper service
	private static final String DAPPER_HOST = "www.dapper.net";
  
	//The path of the Dapper service
  private static final String DAPPER_SERVICE_PATH = "/RunDapp";
  
	//The URL of the Dapp information service
  private static final String DAPP_SERVICE_PATH = "/websiteServices/dapp-json.php";
	
	// Instance variables
	private String xml;
	private Map<Object,Object> dappArray = new HashMap<Object,Object>();
	
	
	//-------------------------------------------------------------------------
	// Constructors
	//-------------------------------------------------------------------------
	
	
	/**
	 * Construct the dapp <code>dappName</code>.
	 * The URL and arguments that will be applied are stored in the dapp.
	 * @throws JSONException 
	 * @throws DappExecutionErrorException 
	 */
	public Dapp(String dappName) throws DappNotFoundException, InvalidVariableArgumentsException, NetworkErrorException, JSONException, DappExecutionErrorException{
		try {
			
			this.setDapperArray(dappName);
			StringBuffer htmlBuffer = new StringBuffer();
			String url;
			
			if(this.dappArray.containsKey("postInfo")){
        JSONObject temp = (JSONObject) this.dappArray.get("postInfo");
				url = (String) temp.get("dapper_originalAction");
				URL parsedUrl = new URL(url);
				Map<String,String> postInfo = this.dissectPostInfo(temp);
				htmlBuffer.append(Util.httpPost(parsedUrl.getHost(), parsedUrl.getPath(), postInfo, null));
				
			} else {
				url = (String) ((JSONArray)this.dappArray.get("dappUrls")).get(0);
				this.storeURLContentsInBuffer(new URL(url), htmlBuffer);	
			}	
			
			getXMLfromServer(dappName, htmlBuffer, url);
			
		} catch (IOException e) {
			throw new NetworkErrorException("Caused by " + e.getClass() + ", " + e.getMessage());
		} 
	}
	
	
	/**
	 * Construct the dapp <code>dappName</code>, and apply it to <code>applyToUrl</code>.
	 * @throws JSONException 
	 * @throws DappExecutionErrorException 
	 */
	public Dapp(String dappName, String applyToUrl) throws DappNotFoundException, InvalidVariableArgumentsException, NetworkErrorException, JSONException, DappExecutionErrorException{
		try {
			
			this.setDapperArray(dappName);
			StringBuffer htmlBuffer = new StringBuffer();
			storeURLContentsInBuffer(new URL(applyToUrl), htmlBuffer);
			getXMLfromServer(dappName, htmlBuffer, applyToUrl);
			
		} catch (IOException e) {
			throw new NetworkErrorException("Caused by " + e.getClass() + ", " + e.getMessage());
		} 
	}
	
	
	
	/**
	 * Construct the POST dapp <code>dappName</code>, specify variable 
	 * arguments using <code>variableArgs</code>.
	 * 
	 * @param variableArgs A map of variable name to argument value
	 * @throws JSONException 
	 * @throws DappExecutionErrorException 
	 */
	public Dapp(String dappName, Map<String,String> variableArgs) throws DappNotFoundException, InvalidVariableArgumentsException, NetworkErrorException, JSONException, DappExecutionErrorException {
		try {
			
			if(variableArgs == null || variableArgs.isEmpty()){
				throw new InvalidVariableArgumentsException("variableArgs argument must not be empty");
			}
			
			this.setDapperArray(dappName);
			
			if (!this.dappArray.containsKey("postInfo")) {
				throw new InvalidVariableArgumentsException("A Map of named variableArgs " +
				"should only be given for POST dapps");
			}
			
			StringBuffer htmlBuffer = new StringBuffer();
			
			// Verify that there is a match in the number of arguments
			if(countVariables() != variableArgs.size()){
				throw new InvalidVariableArgumentsException();
			}
			
			JSONObject postInfo = (JSONObject) this.dappArray.get("postInfo");
			String url = (String) postInfo.get("dapper_originalAction");
			URL parsedUrl = new URL(url);
			Map<String,String> dissectedPostInfo = this.dissectPostInfo(postInfo);
			
			Iterator itr = variableArgs.keySet().iterator();
			while(itr.hasNext()){
				String k = itr.next().toString();
				String v = variableArgs.get(k).toString();
				dissectedPostInfo.put(k,v);
			}
			
			htmlBuffer.append(Util.httpPost(parsedUrl.getHost(), parsedUrl.getPath(), dissectedPostInfo, null));
			
			getXMLfromServer(dappName, htmlBuffer, url);
			
		} catch (IOException e) {
			throw new NetworkErrorException("Caused by " + e.getClass() + ", " + e.getMessage());
		} 
	}
	
	
	
	/**
	 * Construct the GET dapp <code>dappName</code>, specify variable 
	 * arguments using <code>variableArgs</code>.
	 * 
	 * @param variableArgs A list of argument values
	 * @throws JSONException 
	 * @throws DappExecutionErrorException 
	 */
	public Dapp(String dappName, List<String> variableArgs) throws DappNotFoundException, InvalidVariableArgumentsException, NetworkErrorException, JSONException, DappExecutionErrorException {
		try {
			
			if(variableArgs == null || variableArgs.isEmpty()){
				throw new InvalidVariableArgumentsException("variableArgs argument must not be empty");
			}
			
			if (this.dappArray.containsKey("postInfo")) {
				throw new InvalidVariableArgumentsException("variableArgs should be a map " +
				"of named variables in case of a POST dapp");
			}
			
			this.setDapperArray(dappName);
			
			if(countVariables() != variableArgs.size()){
				throw new InvalidVariableArgumentsException();
			}
			
			StringBuffer htmlBuffer = new StringBuffer();
			
			String url = (String) this.dappArray.get("variableUrl");
			url = replaceVarsInGetUrl(url, variableArgs);
			this.storeURLContentsInBuffer(new URL(url), htmlBuffer);
			
			getXMLfromServer(dappName, htmlBuffer, url);
			
		} catch (IOException e) {
			throw new NetworkErrorException("Caused by " + e.getClass() + ", " + e.getMessage());
		} 
	}
	
	
	
	/**
	 * Construct the GET dapp <code>dappName</code>, specify variable 
	 * arguments using <code>variableArgs</code>.
	 * 
	 * @param variableArgs An array of argument values
	 * @throws JSONException 
	 * @throws DappExecutionErrorException 
	 */
	public Dapp(String dappName, String[] variableArgs) throws DappNotFoundException, InvalidVariableArgumentsException, NetworkErrorException, JSONException, DappExecutionErrorException {
		this(dappName,Arrays.asList(variableArgs));
	}
	
	
	//-------------------------------------------------------------------------
	// Private Methods
	//-------------------------------------------------------------------------
	
	
	/**
	 * Get the dapp serialization from the server and store it in an instance
	 * variable. 
	 * @throws JSONException 
	 */
	@SuppressWarnings("unchecked")
  private void setDapperArray(String dappName) throws IOException, DappNotFoundException, JSONException{
		// get the dapp information from the server
		String dappUrlStr;
		
		dappUrlStr = "http://" + DAPPER_HOST + DAPP_SERVICE_PATH +
		"?dappName=" + URLEncoder.encode(dappName, "UTF-8");
		
		URL dappUrl = new URL(dappUrlStr);
		StringBuffer sb = new StringBuffer();
		storeURLContentsInBuffer(dappUrl, sb);
		String dappJSON = sb.toString();
		
		if(dappJSON.indexOf("Dapp does not exist") >= 0){
			throw new DappNotFoundException();
		}
		
		// convert the dapp serialization to a Dapp array
		//dappArray = (Map)PHPSeralization.unserialize(dappSerialization).get(0);
    this.dappArray = new HashMap<Object,Object>();
    JSONObject dappJSONObject = new JSONObject(dappJSON);
    Iterator it = dappJSONObject.keys();
    while (it.hasNext()) {
      String key = (String)it.next();
      dappArray.put(key, dappJSONObject.get(key));
    }
	}
	
	
	
	/**
	 * Get the resulting XML from the server.
	 */
	private void getXMLfromServer(String dappName, StringBuffer htmlBuffer, String applyToUrl) throws IOException, DappExecutionErrorException {
		Map<String,String> data = new HashMap<String,String>();
		data.put("html", htmlBuffer.toString());
		data.put("applyToUrl", applyToUrl);
		data.put("dappName", dappName);
		
    String encoding = null;
    Pattern pat = Pattern.compile("<meta [^>]*charset=([^\"]+)",Pattern.CASE_INSENSITIVE);
    Matcher mat = pat.matcher(htmlBuffer.toString());
    if (mat.find())
      encoding = mat.group(1).trim();
    
		this.xml = Util.httpPost(DAPPER_HOST, DAPPER_SERVICE_PATH, data, encoding);
    
    // if there was an error running the Dapp, find out now
    Pattern errPat = Pattern.compile("<status>ERROR</status><message>([^>]+)</message>");
    Matcher errMat = errPat.matcher(this.xml);
    if (errMat.find())
      throw new DappExecutionErrorException(errMat.group(1));
	}
	
	
	
	/**
	 *  Store the contents of <code>url</code> in <code>sb</code>.
	 */
	private void storeURLContentsInBuffer(URL url, StringBuffer sb) throws IOException {
    sb.append(Util.urlGetContents(url));
	}
	
	
	
	/**
	 * Return the number of variables this dapp takes.
	 */
	private int countVariables() throws MalformedURLException {	
		int count = 0;
		
		// if this is a GET dapp
		if (this.dappArray != null && this.dappArray.containsKey("variableUrl")) {
      String url = (String) this.dappArray.get("variableUrl");
      
      int leftCount = 0;
      int rightCount = 0;
      for (int i = 0; i < url.length(); i++) {
        if (url.charAt(i) == '{')
          leftCount++;
        else if (url.charAt(i) == '}')
          rightCount++;
      }
        
      return Math.min(leftCount, rightCount);
		}
		
		// or if this is a POST dapp
		else if (this.dappArray != null && this.dappArray.containsKey("postVariables")){
			JSONObject postVariables = (JSONObject)this.dappArray.get("postVariables");
			count = postVariables.length();
		}
		
		return count;    
	}
	
	
	
	/**
	 * Takes a URL of the form:<br />
	 * http://someUrl/somePath?v1=arg1&v2=arg2&v3=arg3...<br />
	 * For every <i>arg</i> that starts with <b>{</b> and ends with
	 * <b>}</b>, replace that <i>arg</i> with an element from <code>variableArgs</code>.<br />
	 * 
	 * Requires: The number of <i>args</i> that will be replaced matches 
	 * 			the number of elements in <code>variableArgs</code>.
	 */
	private String replaceVarsInGetUrl(String url, List<String> variableArgs) throws MalformedURLException, UnsupportedEncodingException {
		
		Iterator itr = variableArgs.iterator();
    while (itr.hasNext()) {
      String suppliedVar = (String)itr.next();
      url = url.replaceFirst("\\{[^}]*\\}", URLEncoder.encode(suppliedVar, "UTF-8"));
    }
    return url;
		
    /*
		String originalQS = new URL(url).getQuery();
		StringBuffer newQS = new StringBuffer();
		String[] qStrItems = originalQS.split("&");
		for (int i = 0; i < qStrItems.length; i++) {
			String[] qStrItem = qStrItems[i].split("=");
			String key = qStrItem[0];
			String val = qStrItem[1].trim();
			if(val.startsWith("{") && val.endsWith("}")){		
				val = URLEncoder.encode(itr.next().toString(), "UTF-8");
			}
			newQS.append(key + "=" + val);
			if(i < (qStrItems.length - 1)){
				newQS.append("&");
			}
		}
		
		return url.substring(0,url.indexOf("?")) + "?" + newQS.toString();
    */
	}
	
	
	
	/**
	 * Return a copy of <code>postInfo</code> without all the
	 * keys that start with <i>dapper_</i>
	 * @throws JSONException 
	 */
	private Map<String, String> dissectPostInfo(JSONObject postInfo) throws JSONException {
		
		Map<String,String> dissected = new HashMap<String,String>();
		
		Iterator itr = postInfo.keys();
		while(itr.hasNext()){
			String key = itr.next().toString();
			if(!key.startsWith("dapper_")){
				dissected.put(key, postInfo.get(key).toString());
			}
		}
		return dissected;
	}
	
	
	
	//-------------------------------------------------------------------------
	// Public Methods
	//-------------------------------------------------------------------------
	
	
	/**
	 * Return a Document object to work on the XML
	 * 
	 * @return The XML document object
	 */
	public Document getDOM() throws InvalidDappOutputException {
		try {
			SAXReader saxReader = new SAXReader();
			Document doc;
			
			doc = saxReader.read(new StringReader(this.xml));
			
			return doc;
		} catch (DocumentException e) {
			throw new InvalidDappOutputException();
		}
	}
	
	
	
	/**
	 * Returns the XML output of the Dapp as a string.
	 *
	 * @return string The XML output of the Dapp
	 */
	public String getXML() {
		return this.xml;
	}
	
}
